
#include <cstdlib>
#include <limits>

#include <QDir>
#include <GL/gl.h>
#include <QFile>
#include <QString>
#include <QStringList>
#include <QTextStream>
#include <QVector2D>
#include <QVector3D>

#include <object/face.h>
#include <object/material.h>
#include "object/object.h"

Object::Object()
    : m_bIsValid(false),
      m_minX(std::numeric_limits< qreal >::max()), m_maxX(std::numeric_limits< qreal >::min()),
      m_minY(std::numeric_limits< qreal >::max()), m_maxY(std::numeric_limits< qreal >::min()),
      m_minZ(std::numeric_limits< qreal >::max()), m_maxZ(std::numeric_limits< qreal >::min()),
      m_bIsLoadedGPU(false)
{
}

Object::~Object()
{
    if(m_bIsLoadedGPU)
        unloadGPU();
}

bool Object::isValid() const
{
    return m_bIsValid;
}

bool Object::isLoadedGPU() const
{
    return m_bIsLoadedGPU;
}

qreal Object::width() const
{
    return m_maxX - m_minX;
}

qreal Object::height() const
{
    return m_maxY - m_minY;
}

qreal Object::depth() const
{
    return m_maxZ - m_minZ;
}

qreal Object::minX() const
{
    return m_minX;
}

qreal Object::maxX() const
{
    return m_maxX;
}

qreal Object::minY() const
{
    return m_minY;
}
qreal Object::maxY() const
{
    return m_maxY;
}
qreal Object::minZ() const
{
    return m_minZ;
}
qreal Object::maxZ() const
{
    return m_maxZ;
}

const QVector< Material* >& Object::materials() const
{
    return m_kMaterials;
}

const QVector< const QVector3D* >& Object::vertices() const
{
    return m_kVertices;
}

const QVector< const QVector3D* >& Object::normals() const
{
    return m_kNormals;
}

const QVector< const QVector2D* >& Object::UVs() const
{
    return m_kUVs;
}

const QVector< const Face* >& Object::faces() const
{
    return m_kFaces;
}

bool Object::loadOBJ(const QString &rkFileName)
{
    // Reset invalid state.
    m_bIsValid = false;

    // Open OBJ.
    QFile kFile(rkFileName);
    if(!kFile.open(QIODevice::ReadOnly))
        return false;

    // Set root directory.
    m_kRootDir = QDir::fromNativeSeparators(rkFileName);
    m_kRootDir.truncate(rkFileName.lastIndexOf("/") + 1);

    // Read OBJ file.
    Material* pkCurrentMaterial = NULL;

    QTextStream kText(&kFile);
    while(!kText.atEnd())
    {
        QString kCurrentLine = kText.readLine();
        QStringList kTokens = kCurrentLine.split(QRegExp("\\s+"), QString::SkipEmptyParts);

        // Skip empty lines.
        if(!kTokens.count())
            continue;

        switch(toOBJToken(kTokens[0]))
        {
        case MTL_LIBRARY:
            if(!loadMTL(m_kRootDir + kTokens[1]))
                return false;
            break;

        case MTL_USE:
        {
            int i = 0;
            for(i = 0; i < m_kMaterials.count(); ++i)
            {
                if((m_kMaterials.at(i)->name().compare(kTokens[1])) == 0)
                {
                    pkCurrentMaterial = m_kMaterials.at(i);
                    break;
                }
            }

            // Failed to find the corresponding material.
            if(i == m_kMaterials.count() + 1)
                return false;
            break;
        }

        case VERTEX:
        {
            QVector3D* pkVertex = NULL;
            pkVertex = new QVector3D(kTokens[1].toFloat(),
                                     kTokens[2].toFloat(),
                                     kTokens[3].toFloat());
            Q_CHECK_PTR(pkVertex);
            m_kVertices << pkVertex;

            // Update the object's size.
            m_minX = qMin(m_minX, pkVertex->x());
            m_maxX = qMax(m_maxX, pkVertex->x());
            m_minY = qMin(m_minY, pkVertex->y());
            m_maxY = qMax(m_maxY, pkVertex->y());
            m_minZ = qMin(m_minZ, pkVertex->z());
            m_maxZ = qMax(m_maxZ, pkVertex->z());

            break;
        }

        case UV:
        {
            QVector2D* pkUV = NULL;
            pkUV = new QVector2D(kTokens[1].toFloat(),
                                 kTokens[2].toFloat());
            Q_CHECK_PTR(pkUV);
            m_kUVs << pkUV;
            break;
        }

        case NORMAL:
        {
            QVector3D* pkNormal = NULL;
            pkNormal = new QVector3D(kTokens[1].toFloat(),
                                     kTokens[2].toFloat(),
                                     kTokens[3].toFloat());
            Q_CHECK_PTR(pkNormal);
            m_kNormals << pkNormal;
            break;
        }

        case FACE:
        {
            // Allocate.
            QVector< const QVector3D* > kVertices;
            QVector< const QVector2D* > kUVs;
            QVector< const QVector3D* > kNormals;

            // Fill.
            for(quint32 i = 0; i < static_cast< quint32 >(kTokens.count() - 1); ++i)
            {
                QStringList kFaceTokens = kTokens[i + 1].split(('/'));

                Q_ASSERT(kFaceTokens[0].length());
                kVertices << m_kVertices.at(kFaceTokens[0].toUInt() - 1);

                if(kFaceTokens.count() > 1 && kFaceTokens[1].length())
                    kUVs << m_kUVs.at(kFaceTokens[1].toUInt() - 1);

                if(kFaceTokens.count() > 2 && kFaceTokens[2].length())
                    kNormals << m_kNormals.at(kFaceTokens[2].toUInt() - 1);
            }

            Face* pkFace = NULL;
            pkFace = new Face(kVertices, kNormals, kUVs, pkCurrentMaterial);
            Q_CHECK_PTR(pkFace);
            m_kFaces << pkFace;
            break;
        }

        case OBJ_UNKNOWN:
            break;
        }
    }

    // Squeeze faces.
    m_kFaces.squeeze();

    m_kObjectFileName = rkFileName;

    // Set object as valid.
    foreach(const Material* pkMaterial, m_kMaterials)
        if(!pkMaterial->isValid())
            return false;

    foreach(const Face* pkFace, m_kFaces)
        if(!pkFace->isValid())
            return false;

    m_bIsValid = true;

    return true;
}

void Object::unloadOBJ()
{
    m_bIsValid = false;

    m_kRootDir = "";
    m_kObjectFileName = "";
    m_kMaterialFileName = "";

    // Object data.
    m_minX = std::numeric_limits< qreal >::max(), m_maxX = std::numeric_limits< qreal >::min();
    m_minY = std::numeric_limits< qreal >::max(), m_maxY = std::numeric_limits< qreal >::min();
    m_minZ = std::numeric_limits< qreal >::max(), m_maxZ = std::numeric_limits< qreal >::min();

    foreach(Material* pkMaterial, m_kMaterials)
        delete pkMaterial;

    foreach(const QVector3D* pkVertex, m_kVertices)
        delete pkVertex;

    foreach(const QVector3D* pkNormal, m_kNormals)
        delete pkNormal;

    foreach(const QVector2D* pkUVs, m_kUVs)
        delete pkUVs;

    foreach(const Face* pkFace, m_kFaces)
        delete pkFace;

    m_kMaterials.clear();
    m_kNormals.clear();
    m_kVertices.clear();
    m_kUVs.clear();
    m_kFaces.clear();
}

void Object::loadGPU()
{
    foreach(Material* pkMaterial, materials())
    {
        if(pkMaterial && pkMaterial->isValid())
            pkMaterial->loadGPU();
    }

    m_bIsLoadedGPU = true;
}

void Object::unloadGPU()
{
    foreach(Material* pkMaterial, materials())
    {
        if(pkMaterial && pkMaterial->isValid())
            pkMaterial->unloadGPU();
    }

    m_bIsLoadedGPU = false;
}

Object::OBJToken Object::toOBJToken(const QString& rkToken)
{
    if(rkToken.compare("v") == 0)
        return VERTEX;

    if(rkToken.compare("vt") == 0)
        return UV;

    if(rkToken.compare("vn") == 0)
        return NORMAL;

    if(rkToken.compare("f") == 0)
        return FACE;

    if(rkToken.compare("usemtl") == 0)
        return MTL_USE;

    if(rkToken.compare("mtllib") == 0)
        return MTL_LIBRARY;

    return OBJ_UNKNOWN;
}

Object::MTLToken Object::toMTLToken(const QString& rkToken)
{
    if(rkToken.compare("Ka") == 0)
        return AMBIENT;

    if(rkToken.compare("Kd") == 0)
        return DIFFUSE;

    if(rkToken.compare("Ks") == 0)
        return SPECULAR;

    if(rkToken.compare("d") == 0)
        return SHININESS;

    if(rkToken.compare("map_Ka") == 0)
        return AMBIENT_TEXTURE;

    if(rkToken.compare("map_Kd") == 0)
        return DIFFUSE_TEXTURE;

    if(rkToken.compare("map_Ks") == 0)
        return SPECULAR_TEXTURE;

    return MTL_UNKNOWN;
}

bool Object::loadMTL(const QString& rkFileName)
{
    // Open MTL.
    QFile kFile(rkFileName);
    if(!kFile.open(QIODevice::ReadOnly))
        return false;

    // Read MTL file.
    QTextStream kText(&kFile);
    QString kCurrentLine;
    QStringList kTokens;


    // Search for first material.
    kCurrentLine = kText.readLine();
    kCurrentLine = kCurrentLine.trimmed();

    while(!kText.atEnd() && !kCurrentLine.startsWith("newmtl"))
    {
        kCurrentLine = kText.readLine();
        kCurrentLine = kCurrentLine.trimmed();
    }

    // Find materials.
    while(!kText.atEnd())
    {
        kTokens = kCurrentLine.split(QRegExp("\\s+"), QString::SkipEmptyParts);

        QString kName = kTokens.at(1);
        QString kTextureFileName;
        float   fShininess = 0;
        float   fAmbientRed = 0;
        float   fAmbientGreen = 0;
        float   fAmbientBlue = 0;
        float   fDiffuseRed = 0;
        float   fDiffuseGreen = 0;
        float   fDiffuseBlue = 0;
        float   fSpecularRed = 0;
        float   fSpecularGreen = 0;
        float   fSpecularBlue = 0;

        // Fill material.
        do
        {
            kCurrentLine = kText.readLine();
            kCurrentLine = kCurrentLine.trimmed();
            kTokens = kCurrentLine.split(QRegExp("\\s+"), QString::SkipEmptyParts);

            // Skip empty lines.
            if(!kTokens.count())
                continue;

            switch(toMTLToken(kTokens[0]))
            {
            case AMBIENT:
                fAmbientRed      = kTokens[1].toFloat();
                fAmbientGreen    = kTokens[2].toFloat();
                fAmbientBlue     = kTokens[3].toFloat();

                break;

            case DIFFUSE:
                fDiffuseRed      = kTokens[1].toFloat();
                fDiffuseGreen    = kTokens[2].toFloat();
                fDiffuseBlue     = kTokens[3].toFloat();
                break;

            case SPECULAR:
                fSpecularRed     = kTokens[1].toFloat();
                fSpecularGreen   = kTokens[2].toFloat();
                fSpecularBlue    = kTokens[3].toFloat();
                break;

            case SHININESS:
                fShininess       = kTokens[1].toFloat();
                break;

            case AMBIENT_TEXTURE:
                // Unsupported MTL Token.
                Q_ASSERT(false);

            case DIFFUSE_TEXTURE:
                kTextureFileName = m_kRootDir + kTokens[1];
                break;

            case SPECULAR_TEXTURE:
                // Unsupported MTL Token.
                Q_ASSERT(false);

            case MTL_UNKNOWN:
                break;
            }
        }
        while(!kText.atEnd() && !kCurrentLine.startsWith("newmtl"));

        // Store the new material.
        Material* pkMaterial = NULL;
        pkMaterial = new Material(kName, kTextureFileName, fShininess,
                                  fAmbientRed, fAmbientGreen, fAmbientBlue,
                                  fDiffuseRed, fDiffuseGreen, fDiffuseBlue,
                                  fSpecularRed, fSpecularGreen, fSpecularBlue);
        Q_CHECK_PTR(pkMaterial);
        m_kMaterials << pkMaterial;
    }

    m_kMaterialFileName = rkFileName;
    return true;
}

